package com.cea.papyrus.ocl.validator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.validation.AbstractModelConstraint;
import org.eclipse.emf.validation.IValidationContext;
import org.eclipse.emf.validation.model.ConstraintStatus;
import org.eclipse.ocl.ParserException;
import org.eclipse.ocl.expressions.OCLExpression;
import org.eclipse.ocl.helper.OCLHelper;
import org.eclipse.ocl.uml.OCL;
import org.eclipse.ocl.util.Tuple;
import org.eclipse.uml2.uml.Classifier;
import org.eclipse.uml2.uml.Constraint;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.NamedElement;
import org.eclipse.uml2.uml.Operation;
import org.eclipse.uml2.uml.Package;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Stereotype;
import org.eclipse.uml2.uml.resource.UMLResource;

/*
 * validate elements with regards to the OCL constraints present in one/many of its applied stereotypes.
 */
public class OCLUMLConstraint extends AbstractModelConstraint {

	private static OCL ocl;
	private static OCLHelper<Classifier, Operation, Property, Constraint> oclHelper;
	private  final static HashMap<String,List<Constraint>> stereotypeToConstraints= new HashMap<String,List<Constraint>>();  
	protected static Package umlMetamodel= (Package) new ResourceSetImpl().getResource(
			URI.createURI(UMLResource.UML_METAMODEL_URI),
			true).getContents().get(0);
	private static OCLExpression<Classifier> queryConstraintsParents=null;
	private static OCLExpression<Classifier> queryConstraintsElement=null;
	
	// retrieve the metaclass of a element to provide a proper context
	// to query M2 level.
	protected final Classifier getMetaclass(String name) {
		return (Classifier) umlMetamodel.getOwnedType(name);
	}
	
	/*
	 * validate/evaluate a element with regards to the OCL constraints present in one/many of its applied stereotypes.
	 * (non-Javadoc)
	 * @see org.eclipse.emf.validation.AbstractModelConstraint#validate(org.eclipse.emf.validation.IValidationContext)
	 */
	public IStatus validate(IValidationContext ctx) {
		EObject eObj = ctx.getTarget();

		Element element = null;
		if ((eObj instanceof Element) && (((Element) eObj).getAppliedStereotypes().size()>0)){				
			element = ((Element) eObj);
			if (element.getAppliedStereotypes().get(0).getOwnedRules().size()==0)
				return ctx.createSuccessStatus();
		} else{
			return ctx.createSuccessStatus();
		}

		if (ocl==null){				
			ocl = org.eclipse.ocl.uml.OCL.newInstance();				
			// ocl.setEvaluationTracingEnabled(true);
			// ocl.setParseTracingEnabled(true);
			oclHelper = ocl.createOCLHelper();
		}
		List<IStatus> statuses= new java.util.ArrayList<IStatus>();
		
		for (Stereotype stereotype: element.getAppliedStereotypes()){
			if (stereotype != null){
				for(IStatus status: evaluateConstraintsOn(element, stereotype, ctx)){
					if (!status.isOK())
						statuses.add(status);		
				}
			}
		}
		String namedElement = "emptyName";
		if (element instanceof NamedElement)
			namedElement = ((NamedElement) element).getName();
 		if (!statuses.isEmpty()){
 			return ConstraintStatus.createMultiStatus(ctx, "{0} OCL rule(s) violates element {1}", new Object[] {statuses.size(), namedElement}, statuses);	
 		}else{
 			return ctx.createSuccessStatus();
 		}
	}
	/*
	 * evaluate constraints on a stereotype.
	 */
	private Collection<IStatus> evaluateConstraintsOn(Element element, Stereotype stereotype, IValidationContext ctx){
		List<Constraint> constraints = stereotypeToConstraints.get(stereotype.getName());
		if (constraints == null){
			try {
				Classifier metaClass = getMetaclass("Stereotype");
				oclHelper.setContext(metaClass);
				if (queryConstraintsParents == null){
					try {
						// query to retrieve parent constraints.
						// type of specification is unchecked. We hypothese that they are OCL  OpaqueExpression 
						// I reported bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=194390
						queryConstraintsParents = oclHelper.createQuery("self.allParents().ownedRule->iterate("
								+ " c : Constraint; s : Sequence(Tuple(invariant:String, constraintName:String)) = Sequence{} |"
								//+ "  if c.specification.oclIsKindOf(OpaqueExpression) then"
								+ " if not c.oclIsUndefined() then"
								/*			+	" let e : OpaqueExpression = c.specification.oclAsType(OpaqueExpression),"
								+    " i : Integer = e.language->indexOf('OCL') in"
								+       " if not i.oclIsUndefined() then s->append(e.body->at(i)) else s endif"
								 */ 
								+    " s->append(Tuple{invariant = c.specification.stringValue(), constraintName = c.name})"
								+ " else"
								+ " s"
								+ " endif)");
					} catch (ParserException e1) {
						// TODO Auto-generated catch block
						e1.printStackTrace();
					} //$NON-NLS-1$
				}
				if (queryConstraintsElement == null){
					try {
						queryConstraintsElement= oclHelper.createQuery("self.ownedRule->iterate("
								+ " c : Constraint; s : Sequence(Tuple(invariant:String, constraintName:String)) = Sequence{} |"
								+ " if not c.oclIsUndefined() then"
								+    " s->append(Tuple{invariant = c.specification.stringValue(), constraintName = c.name})"
								+ " else"
								+ " s"
								+ " endif)");
					} catch (ParserException e1) {
						// TODO Auto-generated catch block
						e1.printStackTrace();
					} //$NON-NLS-1$
				}
				
				constraints = createConstraintsFrom(stereotype, queryConstraintsParents);
				constraints.addAll(createConstraintsFrom(stereotype, queryConstraintsElement));
				stereotypeToConstraints.put(stereotype.getName(), constraints);
			} catch (ParserException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		Collection<IStatus> statuses = new java.util.ArrayList<IStatus>();
		for(Constraint constraint: constraints){ 			
			boolean isInvariantSatisfied = ocl.check(element, constraint);
			if (!isInvariantSatisfied){
				statuses.add(ConstraintStatus.createStatus(
						ctx,
						ctx.getTarget().eContents(),
						"Constraint [{0}] in Stereotype {1} is not respected.", 
						new Object[] {constraint.getName(), stereotype.getName()}));				
			}		    			    	
		}
		return statuses;
	}
	
	/* 
	 * gather constraints from a stereotype and its parents.
	 * used by evaluateConstraintsOn to populate/cache a list of constraints.
	 */
	private List<Constraint> createConstraintsFrom(Stereotype stereotype, OCLExpression<Classifier> query) throws ParserException{
		
		Classifier metaClass = getMetaclass("Stereotype");
		oclHelper.setContext(metaClass);

		List<?> result = (List<?>) ocl.evaluate(stereotype, query); //$NON-NLS-1$				
		List<Constraint> constraints = new ArrayList<Constraint>();
		Constraint oclInv=null;
		oclHelper.setContext(stereotype);
		for (Object invariant: result)
			if (invariant instanceof Tuple<?, ?>){
				Tuple tuple = (Tuple) invariant;				
				oclInv =  (Constraint) oclHelper.createInvariant((String) tuple.getValue("invariant"));
				oclInv.setName((String)tuple.getValue("constraintName"));
				constraints.add(oclInv);
			}
		return constraints;
	}
}
